From 02ae938c238c9d18448d17a8ec92c0edd8c17463 Mon Sep 17 00:00:00 2001 From: Bertrand Yuan Date: Tue, 16 Dec 2025 00:12:49 +0800 Subject: feat(back-end): introduce payload Payload is the next.js Headless CMS and App Framework, I would like to pick it up and modify it as it is MIT licensed. Many features in Payload is not applicable for our project. So, I modify it so that it is light and clear. --- src/app/(main)/(home)/posts/[slug]/page.client.tsx | 57 +++++ src/app/(main)/(home)/posts/[slug]/page.tsx | 266 +++++++++++++++++++++ 2 files changed, 323 insertions(+) create mode 100644 src/app/(main)/(home)/posts/[slug]/page.client.tsx create mode 100644 src/app/(main)/(home)/posts/[slug]/page.tsx (limited to 'src/app/(main)/(home)/posts/[slug]') diff --git a/src/app/(main)/(home)/posts/[slug]/page.client.tsx b/src/app/(main)/(home)/posts/[slug]/page.client.tsx new file mode 100644 index 0000000..7a97f56 --- /dev/null +++ b/src/app/(main)/(home)/posts/[slug]/page.client.tsx @@ -0,0 +1,57 @@ +'use client'; +import { + UploadIcon as ShareIcon, + type UploadIconHandle as ShareIconHandle, +} from '@/components/icons/animated/upload'; +import { Icons } from '@/components/icons/icons'; +import { Button } from '@/components/ui/button'; +import { cn } from '@/lib/utils'; +import { Comments } from '@fuma-comment/react'; +import { redirect } from 'next/navigation'; +import { useRef } from 'react'; +import { toast } from 'sonner'; +import { useCopyToClipboard } from 'usehooks-ts'; + +export function Share({ url }: { url: string }): React.ReactElement { + const iconRef = useRef(null); + const [_, copyToClipboard] = useCopyToClipboard(); + + const onClick = async (): Promise => { + await copyToClipboard(`${window.location.origin}${url}`); + toast.success('Copied to clipboard!', { + icon: , + description: 'The post link has been copied to your clipboard.', + }); + }; + + return ( + + ); +} + +export function PostComments({ + slug, + className, +}: { slug: string; className?: string }) { + return ( + { + redirect('/login'); + }, + }} + /> + ); +} diff --git a/src/app/(main)/(home)/posts/[slug]/page.tsx b/src/app/(main)/(home)/posts/[slug]/page.tsx new file mode 100644 index 0000000..fa096b6 --- /dev/null +++ b/src/app/(main)/(home)/posts/[slug]/page.tsx @@ -0,0 +1,266 @@ +import { + PostComments, + Share, +} from '@/app/(main)/(home)/posts/[slug]/page.client'; +import { PostJsonLd } from '@/components/json-ld'; +import { RichText } from '@/components/rich-text'; +import { Section } from '@/components/section'; +import { TagCard } from '@/components/tags/tag-card'; +import { createMetadata } from '@/lib/metadata'; +import { metadataImage } from '@/lib/metadata-image'; +import { + getPostBySlug, + getAllPostSlugs, + type BlogPost, +} from '@/lib/payload-posts'; +import { type Page as MDXPage, getPost, getPosts } from '@/lib/source'; +import { cn } from '@/lib/utils'; +import { File, Files, Folder } from 'fumadocs-ui/components/files'; +import { InlineTOC } from 'fumadocs-ui/components/inline-toc'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; +import defaultMdxComponents from 'fumadocs-ui/mdx'; +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import Balancer from 'react-wrap-balancer'; +import { description as homeDescription } from '@/app/(main)/layout.config'; + +// MDX 文章 Header +function MdxHeader(props: { page: MDXPage; tags?: string[] }) { + const { page, tags } = props; + + return ( +
+
+
+

+ {page.data.title} +

+

+ {page.data.description} +

+
+
+ {tags?.map((tag) => ( + + ))} +
+
+
+ ); +} + +// Payload 文章 Header +function PayloadHeader(props: { post: BlogPost }) { + const { post } = props; + + return ( +
+
+
+

+ {post.title} +

+

+ {post.description} +

+
+
+ {post.tags?.map((tag) => ( + + ))} +
+
+
+ ); +} + +// MDX 文章内容 +function MdxContent({ page }: { page: MDXPage }) { + const { body: Mdx, toc, tags, lastModified } = page.data; + const lastUpdate = lastModified ? new Date(lastModified) : undefined; + + return ( + <> + + +
+
+
+ +
+ +
+ +
+
+
+

Written by

+

{page.data.author}

+
+
+

Created At

+

+ {new Date(page.data.date ?? page.file.name).toDateString()} +

+
+ {lastUpdate && ( +
+

Updated At

+

{lastUpdate.toDateString()}

+
+ )} + +
+
+
+ + + ); +} + +// Payload 文章内容 +function PayloadContent({ post }: { post: BlogPost }) { + return ( + <> + + +
+
+
+ } + className="flex-1 px-4" + enableProse={true} + /> + +
+
+
+

Written by

+

{post.author}

+
+
+

Created At

+

{post.date.toDateString()}

+
+
+

Updated At

+

{post.updatedAt.toDateString()}

+
+ +
+
+
+ + ); +} + +export default async function Page(props: { + params: Promise<{ slug: string }>; +}) { + const params = await props.params; + + // 先尝试获取 MDX 文章 + const mdxPage = getPost([params.slug]); + + if (mdxPage) { + return ; + } + + // 再尝试获取 Payload 文章 + const payloadPost = await getPostBySlug(params.slug); + + if (payloadPost) { + return ; + } + + // 都找不到则 404 + notFound(); +} + +export async function generateMetadata(props: { + params: Promise<{ slug: string }>; +}): Promise { + const params = await props.params; + + // 先尝试 MDX 文章 + const mdxPage = getPost([params.slug]); + + if (mdxPage) { + const title = mdxPage.data.title; + const description = mdxPage.data.description ?? homeDescription; + + return createMetadata( + metadataImage.withImage(mdxPage.slugs, { + title, + description, + openGraph: { + url: `/posts/${mdxPage.slugs.join('/')}`, + }, + alternates: { + canonical: mdxPage.url, + }, + }) + ); + } + + // 再尝试 Payload 文章 + const payloadPost = await getPostBySlug(params.slug); + + if (payloadPost) { + return createMetadata({ + title: payloadPost.title, + description: payloadPost.description || homeDescription, + openGraph: { + url: `/posts/${payloadPost.slug}`, + }, + alternates: { + canonical: `/posts/${payloadPost.slug}`, + }, + }); + } + + return {}; +} + +export async function generateStaticParams(): Promise<{ slug: string }[]> { + // MDX 文章的 slugs + const mdxSlugs = getPosts() + .map((page) => page.slugs[0]) + .filter((slug): slug is string => !!slug) + .map((slug) => ({ slug })); + + // Payload 文章的 slugs + const payloadSlugs = await getAllPostSlugs(); + const payloadParams = payloadSlugs.map((slug) => ({ slug })); + + return [...mdxSlugs, ...payloadParams]; +} -- cgit v1.2.3